Passed
Push — master ( 6ceee8...d4902a )
by Tristan
20:51 queued 08:57
created

Modal.tsx ➔ Modal   D

Complexity

Conditions 11

Size

Total Lines 117
Code Lines 87

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 87
dl 0
loc 117
rs 4.2054
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Modal.tsx ➔ Modal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import React, { createContext, useContext, useEffect, useRef } from "react";
2
import { createPortal } from "react-dom";
3
4
interface ModalProps {
5
  id: string;
6
  parentElement: Element | null;
7
  visible: boolean;
8
  children: React.ReactNode;
9
  onModalConfirm: (e: React.MouseEvent<HTMLButtonElement>) => void;
10
  onModalMiddle?: (e: React.MouseEvent<HTMLButtonElement>) => void;
11
  onModalCancel: (
12
    e: React.MouseEvent<HTMLButtonElement> | KeyboardEvent,
13
  ) => void;
14
}
15
16
// Partial helper allows empty defaults in the createContext call:
17
// https://fettblog.eu/typescript-react/context/#context-without-default-values
18
const modalContext = createContext<Partial<ModalProps>>({});
19
20
export default function Modal({
21
  id,
22
  parentElement,
23
  visible,
24
  children,
25
  onModalConfirm,
26
  onModalMiddle,
27
  onModalCancel,
28
}: ModalProps): React.ReactPortal | null {
29
  // Set up div ref to measure modal height
30
  const modalRef = useRef<HTMLDivElement>(null);
31
32
  const handleTabKey = (e: KeyboardEvent): void => {
33
    if (modalRef && modalRef.current) {
34
      const focusableModalElements = modalRef.current.querySelectorAll(
35
        'a[href], button, textarea, input[type="text"], input[type="email"], input[type="radio"], select',
36
      );
37
      const firstElement = focusableModalElements[0] as HTMLElement;
38
      const lastElement = focusableModalElements[
39
        focusableModalElements.length - 1
40
      ] as HTMLElement;
41
42
      const focusableModalElementsArray = Array.from(focusableModalElements);
43
44
      if (
45
        document.activeElement &&
46
        !focusableModalElementsArray.includes(document.activeElement)
47
      ) {
48
        firstElement.focus();
49
        e.preventDefault();
50
      }
51
52
      if (!e.shiftKey && document.activeElement === lastElement) {
53
        firstElement.focus();
54
        e.preventDefault();
55
      }
56
57
      if (e.shiftKey && document.activeElement === firstElement) {
58
        lastElement.focus();
59
        e.preventDefault();
60
      }
61
    }
62
  };
63
64
  // Collection of key codes and event listeners
65
  const keyListenersMap = new Map([
66
    [27, onModalCancel],
67
    [9, handleTabKey],
68
  ]);
69
70
  // Runs every time visible changes to set the overflow on the modal and update the body overflow
71
  useEffect((): (() => void) => {
72
    function setBodyStyle(): void {
73
      document.body.style.overflow = visible ? "hidden" : "visible";
74
    }
75
    setBodyStyle();
76
    // Runs on component unmount
77
    return (): void => {
78
      setBodyStyle();
79
    };
80
  }, [visible]);
81
82
  // Adds various key commands to the modal
83
  useEffect((): (() => void) => {
84
    let keyListener;
85
    if (visible) {
86
      keyListener = (e: KeyboardEvent): void => {
87
        const listener = keyListenersMap.get(e.keyCode);
88
        return listener && listener(e);
89
      };
90
      document.addEventListener("keydown", keyListener);
91
    }
92
93
    return (): void => {
94
      if (keyListener !== undefined) {
95
        document.removeEventListener("keydown", keyListener);
96
      }
97
    };
98
  }, [keyListenersMap, visible]);
99
100
  if (parentElement !== null) {
101
    return createPortal(
102
      <div
103
        aria-describedby={`${id}-description`}
104
        aria-hidden={!visible}
105
        aria-labelledby={`${id}-title`}
106
        data-c-dialog={visible ? "active--overflowing" : ""}
107
        data-c-padding="top(double) bottom(double)"
108
        role="dialog"
109
        ref={modalRef}
110
        style={{
111
          display: "flex",
112
          alignItems: "center",
113
          justifyContent: "center",
114
        }}
115
      >
116
        <div data-c-background="white(100)" data-c-radius="rounded">
117
          <modalContext.Provider
118
            value={{
119
              id,
120
              parentElement,
121
              visible,
122
              onModalConfirm,
123
              onModalMiddle,
124
              onModalCancel,
125
            }}
126
          >
127
            {children}
128
          </modalContext.Provider>
129
        </div>
130
      </div>,
131
      parentElement,
132
    );
133
  }
134
135
  return null;
136
}
137
138
Modal.Header = function ModalHeader(props): React.ReactElement {
139
  return props.children;
140
};
141
142
Modal.Body = function ModalBody(props): React.ReactElement {
143
  const { children } = props;
144
  return <div data-c-border="bottom(thin, solid, black)">{children}</div>;
145
};
146
147
Modal.Footer = function ModalFooter(props): React.ReactElement {
148
  const { children } = props;
149
  return (
150
    <div data-c-padding="normal">
151
      <div data-c-grid="gutter middle">
152
        {Array.isArray(children) && children.length > 0
153
          ? children.map(
154
              (btn, index): React.ReactElement => (
155
                <div
156
                  // eslint-disable-next-line react/no-array-index-key
157
                  key={index}
158
                  data-c-grid-item={`base(1of${children.length})`}
159
                >
160
                  {btn}
161
                </div>
162
              ),
163
            )
164
          : children}
165
      </div>
166
    </div>
167
  );
168
};
169
170
Modal.FooterConfirmBtn = function ConfirmBtn(props): React.ReactElement {
171
  const { onModalConfirm } = useContext(modalContext);
172
  return (
173
    <div data-c-alignment="base(right)">
174
      <button
175
        {...props}
176
        data-c-button="solid(c1)"
177
        data-c-dialog-action="close"
178
        data-c-radius="rounded"
179
        type="button"
180
        onClick={onModalConfirm}
181
      />
182
    </div>
183
  );
184
};
185
186
Modal.FooterCancelBtn = function CancelBtn(props): React.ReactElement {
187
  const { onModalCancel } = useContext(modalContext);
188
  return (
189
    <div>
190
      <button
191
        {...props}
192
        data-c-button="outline(c1)"
193
        data-c-dialog-action="close"
194
        data-c-radius="rounded"
195
        type="button"
196
        onClick={onModalCancel}
197
      />
198
    </div>
199
  );
200
};
201
202
Modal.FooterMiddleBtn = function MiddleBtn(props): React.ReactElement {
203
  const { onModalMiddle } = useContext(modalContext);
204
  return (
205
    <div data-c-alignment="base(center)">
206
      <button
207
        {...props}
208
        data-c-button="solid(c1)"
209
        data-c-dialog-action="close"
210
        data-c-radius="rounded"
211
        type="button"
212
        disabled={onModalMiddle === undefined}
213
        onClick={onModalMiddle}
214
      />
215
    </div>
216
  );
217
};
218